<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第九章：实际应用案例 - 9.1 构建一个简单的问答系统</title>
    <link rel="stylesheet" href="../shared_styles.css">
</head>
<body>
    <div class="container">
        <h1> 第九章：实际应用案例</h1>
        <section id="s9-1">
            <h2>9.1 构建一个简单的问答系统 (RAG-based)</h2>
            <p>在本节中，我们将结合前面学到的知识，特别是第七章中关于Indexes和RAG（检索增强生成）的内容，来构建一个简单的问答（QA）系统。这个系统将能够根据我们提供的一组文档来回答问题。</p>
            <p>我们的目标是创建一个能够执行以下操作的流程：</p>
            <ol>
                <li>加载自定义文档数据。</li>
                <li>将文档分割成合适的文本块。</li>
                <li>为这些文本块创建向量嵌入并存储在向量数据库中。</li>
                <li>当用户提出问题时，检索与问题最相关的文档块。</li>
                <li>将检索到的文档块和用户问题一起传递给Qwen LLM，生成答案。</li>
            </ol>
            <p>这是一个典型的RAG流程，非常适合构建基于特定知识库的问答机器人。</p>

            <h3>步骤1：准备环境和数据</h3>
            <p>首先，确保你已经安装了必要的库，并设置了API密钥。我们还需要一些示例文本数据。</p>
            <pre><code class="language-python">
# 所需库: langchain, langchain_openai, langchain_community, faiss-cpu, python-dotenv, pypdf (如果用PDF)
# pip install langchain langchain_openai langchain_community faiss-cpu python-dotenv

from langchain_openai import ChatOpenAI # Qwen兼容
from langchain_community.embeddings import DashScopeEmbeddings # Qwen兼容嵌入
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
import os

# 加载环境变量 (确保你的 .env 文件中有 DASHSCOPE_API_KEY)
load_dotenv()
api_key = os.environ.get("DASHSCOPE_API_KEY")
if not api_key:
    raise ValueError("DASHSCOPE_API_KEY not found in environment variables. Please set it in your .env file.")

# 创建一些示例文本文件作为我们的知识库
knowledge_base_texts = {
    "langchain_overview.txt": """Langchain是一个用于开发由语言模型驱动的应用程序的框架。
它使得开发者能够轻松地将大型语言模型（LLM）与其他计算或知识来源集成。
核心组件包括模型I/O、数据连接（Data Connection）、链（Chains）、代理（Agents）、内存（Memory）和回调（Callbacks）。
这些组件是模块化的，可以组合使用来创建复杂的应用程序。
例如，可以创建一个链，首先从用户输入生成一个提示，然后将该提示发送给LLM，最后处理LLM的输出。
RAG（检索增强生成）是Langchain的一个关键应用，它允许LLM访问外部数据以提供更准确和最新的答案。
Langchain支持多种LLM，包括OpenAI模型以及像Qwen这样的开源和商业模型。
通过数据连接，可以加载、转换和存储数据，为LLM提供上下文。
链允许将多个调用（对LLM或其他工具）串联起来。
代理则赋予LLM动态决策和使用工具的能力。
内存机制使得应用能够记住先前的交互。
回调系统则用于监控和记录应用的内部运作。
""",
    "qwen_model_intro.txt": """Qwen（千问）是阿里云开发的一系列大型语言模型。
Qwen系列包括不同参数规模的模型，如Qwen-Turbo、Qwen-Plus和Qwen-Max，以适应不同的应用需求和成本考虑。
这些模型在多种自然语言处理任务上表现出色，包括文本生成、问答、摘要、翻译等。
Qwen模型可以通过阿里云的DashScope服务平台进行API调用。
Langchain框架也提供了对Qwen模型的集成，使得开发者可以方便地在Langchain应用中使用Qwen的强大能力。
例如，可以使用ChatTongyi或通过兼容OpenAI的接口来调用Qwen聊天模型，
并使用DashScopeEmbeddings来获取Qwen的文本嵌入。
Qwen模型支持长上下文处理，并且在中文和英文语料上都有很好的表现。
""",
    "rag_explanation.txt": """检索增强生成（Retrieval Augmented Generation，RAG）是一种结合了检索系统和生成模型的AI技术。
它的核心思想是，在让大型语言模型（LLM）生成答案之前，先从一个大规模的知识库中检索出与用户问题相关的文档片段。
然后，这些检索到的文档片段会作为额外的上下文信息，与原始问题一起提供给LLM。
通过这种方式，LLM可以基于这些具体、相关的上下文来生成更准确、更具事实依据、更符合特定领域知识的回答。
RAG流程通常包括：文档加载、文本分割、向量嵌入、索引构建、查询时检索和答案生成。
它有效地解决了LLM知识截止和幻觉（hallucination）问题。
"""
}

# 将示例文本写入文件
for filename, content in knowledge_base_texts.items():
    with open(filename, "w", encoding="utf-8") as f:
        f.write(content)
print("示例知识库文件创建完毕。")
            </code></pre>

            <h3>步骤2：加载和分割文档</h3>
            <p>我们将加载创建的文本文件，并将它们分割成小块。</p>
            <pre><code class="language-python">
# 加载所有示例文本文件
all_documents = []
for filename in knowledge_base_texts.keys():
    loader = TextLoader(f"./{filename}", encoding="utf-8")
    all_documents.extend(loader.load())

print(f"\n总共加载了 {len(all_documents)} 个文档（每个文件视为一个文档）。")

# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  # 目标块大小 (字符数)
    chunk_overlap=50, # 块间重叠
    length_function=len,
)

# 分割文档
all_chunks = text_splitter.split_documents(all_documents)
print(f"所有文档被分割成了 {len(all_chunks)} 个文本块。")
# print(f"第一个块的内容示例:\n{all_chunks[0].page_content}")
            </code></pre>

            <h3>步骤3：创建文本嵌入并构建向量存储</h3>
            <p>使用Qwen的嵌入模型和FAISS向量存储。</p>
            <pre><code class="language-python">
# 初始化Qwen嵌入模型
qwen_embeddings = DashScopeEmbeddings(
    dashscope_api_key=api_key,
    model="text-embedding-v1" 
)

# 使用FAISS从文档块创建向量存储
# 这会自动处理文本块的嵌入过程
print("\n正在创建FAISS向量存储...")
try:
    vector_store = FAISS.from_documents(documents=all_chunks, embedding=qwen_embeddings)
    print("FAISS向量存储创建成功！")
except Exception as e:
    print(f"创建FAISS索引时出错: {e}")
    vector_store = None # 确保后续代码不会因vector_store未定义而出错
            </code></pre>

            <h3>步骤4：创建检索器</h3>
            <p>从向量存储创建一个检索器，用于根据查询获取相关文档块。</p>
            <pre><code class="language-python">
if vector_store:
    retriever = vector_store.as_retriever(
        search_type="similarity", # 可以是 "similarity", "mmr", "similarity_score_threshold"
        search_kwargs={"k": 3}    # 每次检索返回最相关的3个块
    )
    print("从FAISS创建检索器成功。")
else:
    retriever = None
    print("由于向量存储创建失败，检索器未能创建。")
            </code></pre>

            <h3>步骤5：初始化Qwen LLM和定义RAG提示模板</h3>
            <p>我们将使用Qwen聊天模型，并定义一个提示模板，该模板指示模型如何使用检索到的上下文来回答问题。</p>
            <pre><code class="language-python">
# 初始化Qwen聊天模型
qwen_llm = ChatOpenAI(
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=api_key,
    model="qwen-plus", # 或 "qwen-turbo", "qwen-max"
    temperature=0.2,   # 对于基于事实的问答，较低的温度通常更好
    streaming=False    # 对于简单问答，可以不开启流式
)
print("Qwen LLM 初始化成功。")

# 定义RAG提示模板
rag_template_str = """
你是一个智能问答助手。请严格根据下面提供的上下文信息来回答用户的问题。
如果上下文中没有足够的信息来回答问题，请直接说“根据我目前掌握的信息，我无法回答您的问题”，不要试图编造答案。
请确保答案简洁明了。

上下文:
{context}

用户问题:
{question}

回答:
"""
rag_prompt_template = ChatPromptTemplate.from_template(rag_template_str)
            </code></pre>

            <h3>步骤6：构建并运行RAG链</h3>
            <p>使用Langchain表达式语言（LCEL）将所有组件连接起来。</p>
            <pre><code class="language-python">
def format_docs_for_context(docs: list) -> str:
    """辅助函数，将检索到的文档列表格式化为单个字符串上下文。"""
    return "\n\n".join([doc.page_content for doc in docs])

if retriever and qwen_llm:
    # 构建RAG链
    rag_chain = (
        RunnablePassthrough.assign(
            # retriever.invoke(x["question"]) 会根据问题从向量库检索文档
            # format_docs_for_context 将文档列表转换为字符串
            context=(lambda x: format_docs_for_context(retriever.invoke(x["question"])))
        )
        | rag_prompt_template # 将包含 question 和 context 的字典传递给提示模板
        | qwen_llm            # 将格式化后的提示传递给LLM
        | StrOutputParser()   # 获取LLM输出的字符串内容
    )
    print("RAG链构建成功！")

    # 测试问答系统
    questions_to_ask = [
        "Langchain的核心组件有哪些？",
        "Qwen模型是什么？它有哪些不同规模？",
        "RAG技术是如何工作的？请简要解释。",
        "Langchain支持哪些类型的LLM？",
        "什么是FAISS？" # 这个问题在我们的示例文档中没有直接答案
    ]

    print("\n--- 开始问答测试 ---")
    for i, question in enumerate(questions_to_ask):
        print(f"\n问题 {i+1}: {question}")
        try:
            answer = rag_chain.invoke({"question": question})
            print(f"AI 回答:\n{answer}")
        except Exception as e:
            print(f"处理问题时发生错误: {e}")
        print("-" * 30)
else:
    print("由于检索器或LLM未能初始化，RAG链未构建，无法进行问答测试。")

# 清理创建的示例文件
for filename in knowledge_base_texts.keys():
    if os.path.exists(filename):
        os.remove(filename)
print("\n示例知识库文件已清理。")
            </code></pre>
            <p>当你运行完整的脚本时，它会：</p>
            <ol>
                <li>创建示例文本文件。</li>
                <li>加载、分割这些文件，并用Qwen嵌入模型和FAISS构建向量索引。</li>
                <li>创建一个检索器。</li>
                <li>初始化Qwen LLM。</li>
                <li>构建一个RAG链。</li>
                <li>针对一系列预设问题调用RAG链，并打印LLM基于检索上下文生成的回答。</li>
                <li>最后清理示例文件。</li>
            </ol>
            <p>对于我们知识库中没有明确答案的问题（如“什么是FAISS？”），理想情况下，Qwen模型应该根据提示中的指示，回答它无法从已知信息中找到答案。</p>
            <p>这个简单的问答系统展示了RAG的基本流程。在实际应用中，你可能需要处理更复杂的文档、使用更高级的检索策略、优化提示、评估系统性能等。</p>
        </section>
        
        <div class="navigation">
            <a href="../langchain_tutorial_outline.html">返回教程目录</a>
            <a href="../chapter8/chapter8_callbacks.html">上一章：Callbacks模块</a>
            <!-- <a href="../chapter9/chapter9_application_text_generation.html">下一节：9.2 实现自动化文本生成</a> -->
        </div>
    </div>
</body>
</html>
